package cmd

import (
	ckeadmv1beta1 "ckeadm/cmd/apis/v1alpha1"
	"ckeadm/cmd/runner"
	"ckeadm/constants"
	"ckeadm/util/runtime"
	"encoding/json"
	"fmt"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	utilsexec "k8s.io/utils/exec"
	"os"
	"os/exec"
	"strings"
)

func DiffCommand() cli.Command {
	var diffFlags = []cli.Flag{
		runner.FromVersionFlag,
		runner.ToVersionFlag,
		runner.RegistryFlag,
	}

	return cli.Command{
		Name:   "diff",
		Usage:  "find different components",
		Action: runDiff,
		Flags:  diffFlags,
	}
}

func runDiff(ctx *cli.Context) error {
	var err error

	fromVersion := ctx.String("fromVersion")
	toVersion := ctx.String("toVersion")
	if fromVersion == "" || toVersion == "" {
		return fmt.Errorf("find different components failed, fromVersion or toVersion is empty")
	}
	clean(fromVersion, toVersion)
	logrus.Infof("start to find different components between %s and %s", fromVersion, toVersion)

	defer clean(fromVersion, toVersion)

	logrus.Infof("copy the crs from image: %s", getImage(ctx.String("registry"), "library/cke/cke-admin", fromVersion))
	err = extractCRs(fromVersion, getImage(ctx.String("registry"), "library/cke/cke-admin", fromVersion))
	if err != nil {
		return fmt.Errorf("error while extract crs from image: %s", err.Error())
	}

	logrus.Infof("copy the crs from image: %s", getImage(ctx.String("registry"), "library/cke/cke-admin", toVersion))
	err = extractCRs(toVersion, getImage(ctx.String("registry"), "library/cke/cke-admin", toVersion))
	if err != nil {
		return fmt.Errorf("error while extract crs from image: %s", err.Error())
	}

	var fComponents = new([]ckeadmv1beta1.CkeComponent)
	var tComponents = new([]ckeadmv1beta1.CkeComponent)

	err = runner.GetCkeComponentFromFile(constants.CkeDeployConfigPath + "/" + fromVersion, fComponents)
	if err != nil {
		return fmt.Errorf("failed to get fComponents: %s", err.Error())
	}

	err = runner.GetCkeComponentFromFile(constants.CkeDeployConfigPath + "/" + toVersion, tComponents)
	if err != nil {
		return fmt.Errorf("failed to get tComponents: %s", err.Error())
	}

	addComponents, delComponents, needUpComponents, hasDiff := diff(fComponents, tComponents)
	if !hasDiff {
		logrus.Infof("there are no different components between %s and %s", fromVersion, toVersion)
		return nil
	}

	if len(addComponents) > 0 {
		logrus.Infof("addComponents: %s", strings.Join(addComponents, ","))
	}
	if len(delComponents) > 0 {
		logrus.Infof("delComponents: %s", strings.Join(delComponents, ","))
	}
	if len(needUpComponents) > 0 {
		needUpComponentsName := make([]string, 0)
		for _, needUpComponent := range needUpComponents {
			needUpComponentsName = append(needUpComponentsName, needUpComponent[0].Name)
		}
		logrus.Infof("needUpComponents: %s", strings.Join(needUpComponentsName, ","))
		for _, needUpComponent := range needUpComponents {
			logrus.Infof("%s", needUpComponent[0].Name)
			if needUpComponent[0].Spec.Version != needUpComponent[1].Spec.Version {
				logrus.Infof("old version: \"%s\"", needUpComponent[0].Spec.Version)
				logrus.Infof("new version: \"%s\"", needUpComponent[1].Spec.Version)
			}
			if needUpComponent[0].Spec.ImageTag != needUpComponent[1].Spec.ImageTag {
				logrus.Infof("old imageTag: \"%s\"", needUpComponent[0].Spec.ImageTag)
				logrus.Infof("new imageTag: \"%s\"", needUpComponent[1].Spec.ImageTag)
			}
			if IsParametersChanged(needUpComponent[0], needUpComponent[1]) {
				logrus.Infof("old parameters: %#v", needUpComponent[0].Spec.Parameters)
				logrus.Infof("new parameters: %#v", needUpComponent[1].Spec.Parameters)
			}
		}
	}

	return nil
}

func getImage(registry, body, tag string) string {
	return fmt.Sprintf("%s/%s:%s", registry, fmt.Sprintf("%s-%s", body, constants.ARCH), tag)
}

func extractCRs(version, imageName string) error {
	dockerRunTime, err := runtime.NewContainerRuntime(utilsexec.New())
	utilruntime.HandleError(err)

	if exist, err := dockerRunTime.ImageExists(imageName); err != nil || !exist {
		err = dockerRunTime.PullImage(imageName)
		if err != nil {
			return fmt.Errorf("fail pull images: %s, err: %s", imageName, err.Error())
		}
	}

	//清空对应插件子目录
	_ = os.RemoveAll(constants.CkeDeployConfigPath + "/" + version)

	dockerArgs := []string{
		"run",
		"--rm",
		"-v", constants.CkeDeployConfigPath + ":" + "/var",
	}

	var running = exec.Command("docker", append(dockerArgs, imageName, "/bin/cp", "-rf", constants.CkeDeployConfigPath+"/component-template", "/var/"+version)...)

	_, err = running.CombinedOutput()
	return err
}

func diff(fComponents, tComponents *[]ckeadmv1beta1.CkeComponent) (addComponents, delComponents []string, needUpComponents [][]ckeadmv1beta1.CkeComponent, hasDiff bool) {
	fComponentsMap := make(map[string]ckeadmv1beta1.CkeComponent)
	tComponentsMap := make(map[string]ckeadmv1beta1.CkeComponent)

	for _, fComponent := range *fComponents {
		fComponentsMap[fComponent.Name] = fComponent
	}

	for _, tComponent := range *tComponents {
		tComponentsMap[tComponent.Name] = tComponent
	}

	for name, tComponent := range tComponentsMap {
		if fComponent, ok := fComponentsMap[name]; !ok {
			addComponents = append(addComponents, name)
		} else {
			if fComponent.Spec.Version != tComponent.Spec.Version || fComponent.Spec.ImageTag != tComponent.Spec.ImageTag || IsParametersChanged(fComponent, tComponent) {
				needUpComponents = append(needUpComponents, []ckeadmv1beta1.CkeComponent{fComponent, tComponent})
			}
		}
	}

	for name, _ := range fComponentsMap {
		if _, ok := tComponentsMap[name]; !ok {
			delComponents = append(delComponents, name)
		}
	}

	if len(addComponents) > 0 || len(delComponents) > 0 || len(needUpComponents) > 0 {
		hasDiff = true
	}

	return
}

func IsParametersChanged(fComponent, tComponent ckeadmv1beta1.CkeComponent) bool {
	isParametersChanged := false
	fComponentParameters := make(map[string]string)
	if len(fComponent.Spec.Parameters) != len(tComponent.Spec.Parameters) {
		isParametersChanged = true
	} else {
		date, _ := json.Marshal(fComponent.Spec.Parameters)
		_ = json.Unmarshal(date, &fComponentParameters)
		for k, v := range tComponent.Spec.Parameters {
			if fComponentParameters[k] == v {
				delete(fComponentParameters, k)
			} else {
				break
			}
		}
		if len(fComponentParameters) > 0 {
			isParametersChanged = true
		}
	}
	return isParametersChanged
}

func clean(fromVersion, toVersion string) {
	_ = os.RemoveAll(constants.CkeDeployConfigPath + "/" + fromVersion)
	_ = os.RemoveAll(constants.CkeDeployConfigPath + "/" + toVersion)
}
